Add chat gateway#2
Conversation
There was a problem hiding this comment.
4 issues found across 19 files
Confidence score: 2/5
- In
apps/server/src/gateway/Gateway.ts, usingStream.mergeAllwithout per-adapter error isolation means one transient adapter failure can terminate the entire incoming pipeline, potentially stopping message intake system-wide—add adapter-level catch/restart behavior (or supervised retries) before merging. - In
apps/server/src/gateway/adapters/telegram/TelegramAdapter.ts, retry delay capping can ignore a configuredretryMaxDelayMilliswhen it is below the base delay, so polling may back off longer than operators expect during incidents—enforce the configured max as a hard cap (or fail fast on invalid config) before merging. - In
apps/server/src/gateway/adapters/telegram/TelegramModels.ts, schema drift from Telegram’s contract (file_unique_idmarked optional, integer-only fields accepted as any finite number) can mask payload/data-shape problems and cause subtle downstream assumptions to break—tighten these model constraints or document and test the intentional looseness before merging.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/server/src/gateway/adapters/telegram/TelegramAdapter.ts">
<violation number="1" location="apps/server/src/gateway/adapters/telegram/TelegramAdapter.ts:286">
P2: A configured retry maximum below the base delay is ignored, so polling can wait longer than `retryMaxDelayMillis`. Use the configured maximum as the cap (or reject this invalid config combination).</violation>
</file>
<file name="apps/server/src/gateway/Gateway.ts">
<violation number="1" location="apps/server/src/gateway/Gateway.ts:56">
P1: Missing error isolation in merged incoming stream: `Stream.mergeAll` terminates the entire stream when any single adapter stream fails. A transient network error in the Telegram adapter would cut off incoming messages from all adapters. Wrap each adapter's stream with `Stream.catchAll` or `Stream.catchTag` to isolate failures per adapter (log and resume, or restart the adapter stream) so one adapter's error doesn't bring down the whole gateway.</violation>
</file>
<file name="apps/server/src/gateway/adapters/telegram/TelegramModels.ts">
<violation number="1" location="apps/server/src/gateway/adapters/telegram/TelegramModels.ts:5">
P2: `file_unique_id` in `TelegramFileFields` is modeled with `Schema.optionalKey`, but the Telegram Bot API guarantees `file_unique_id` is always present on all file objects (`File`, `PhotoSize`, `Document`, `Audio`, `Video`, `Voice`). This means the TypeScript type will have `file_unique_id?: string | undefined` when Telegram never omits it, forcing unnecessary defensive `??` / `?.` checks in consumer code. Use `Schema.String` instead.</violation>
<violation number="2" location="apps/server/src/gateway/adapters/telegram/TelegramModels.ts:10">
P3: Numeric fields like `id`, `message_id`, `date`, `width`, `height`, `duration`, and `file_size` use `Schema.Finite`, which accepts any finite number including floats. The Telegram Bot API always returns integers for these fields. The codebase already has `const NonNegativeInteger = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))` in `packages/contracts/src/gateway.ts` — using that pattern (or at minimum `Schema.Finite.check(Schema.isInt())`) here would catch non-integer values that `Schema.Finite` silently accepts.</violation>
</file>
Architecture diagram
sequenceDiagram
participant App as Application
participant Gateway as Gateway Service
participant Config as GatewayConfig
participant Adapter as TelegramAdapter
participant Api as TelegramApi
participant Files as GatewayFiles
participant TelegramAPI as Telegram Bot API
Note over App,TelegramAPI: Incoming message flow (long polling)
App->>Gateway: subscribe to incoming stream
Gateway->>Adapter: merge adapter streams
loop Poll loop
Adapter->>Api: getUpdates(offset, timeout, limit)
Api->>TelegramAPI: HTTP POST /getUpdates
TelegramAPI-->>Api: array of updates
Api-->>Adapter: raw updates
Adapter->>Adapter: decode & filter supported messages
alt Valid message with content
Adapter->>Adapter: create IncomingAdapterEvent (channel, lazy materialize)
Adapter-->>Gateway: emit event
Gateway->>Config: check allowedChannels
alt Channel allowed
Gateway->>Adapter: call materialize
Adapter->>Api: getFile(fileId)
Api->>TelegramAPI: HTTP GET /getFile
TelegramAPI-->>Api: file metadata (path, size)
Api-->>Adapter: TelegramFile
alt file_size <= maxInboundFileBytes
Adapter->>Api: download(filePath) – stream
Api->>TelegramAPI: HTTP GET /file/bot{token}/{path}
TelegramAPI-->>Api: byte stream
Adapter->>Files: saveStream(stream, preferredName, kind, maxBytes)
Files->>Files: atomic write to ~/.compass/files/<uuid>-<name>
Files-->>Adapter: GatewayFile
Adapter->>Adapter: construct IncomingMessage (text, files, sender)
Adapter-->>Gateway: IncomingMessage
Gateway-->>App: push to incoming stream
else file_size > limit
Adapter-->>Gateway: GatewayFileError
Gateway->>Gateway: log warning, skip message
end
else Channel denied
Gateway->>Gateway: filter event – materialize NOT called
end
else Malformed or unsupported update
Adapter->>Adapter: log warning, advance offset
end
end
Note over App,TelegramAPI: Outgoing message flow (send)
App->>Gateway: send(OutgoingMessage)
Gateway->>Config: check allowedChannels
alt Channel not allowed
Gateway-->>App: GatewayAuthorizationError
else Channel allowed
Gateway->>Gateway: validate message (text or files)
alt Invalid (empty text + no files)
Gateway-->>App: GatewayInvalidMessageError
else Valid
Gateway->>Gateway: parseChannelId -> adapter name
alt Adapter not found
Gateway-->>App: GatewayAdapterNotFoundError
else Adapter found
Gateway->>Adapter: send(message)
Adapter->>Adapter: parse thread id
alt Has files
loop For each file (concurrency=1)
Adapter->>Files: read(GatewayFile)
Files-->>Adapter: bytes
Adapter->>Api: sendMedia(method, field, chatId, bytes, caption)
Api->>TelegramAPI: HTTP POST /sendPhoto|/sendAudio|...
TelegramAPI-->>Api: TelegramMessage
Api-->>Adapter: message_id
end
Adapter-->>Gateway: SentMessage (array of messageIds)
else Text only
Adapter->>Api: sendText(chatId, threadId, text)
Api->>TelegramAPI: HTTP POST /sendMessage
TelegramAPI-->>Api: TelegramMessage
Api-->>Adapter: message_id
Adapter-->>Gateway: SentMessage
end
Gateway-->>App: SentMessage
end
end
end
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
| }); | ||
| } | ||
|
|
||
| const incoming = Stream.mergeAll( |
There was a problem hiding this comment.
P1: Missing error isolation in merged incoming stream: Stream.mergeAll terminates the entire stream when any single adapter stream fails. A transient network error in the Telegram adapter would cut off incoming messages from all adapters. Wrap each adapter's stream with Stream.catchAll or Stream.catchTag to isolate failures per adapter (log and resume, or restart the adapter stream) so one adapter's error doesn't bring down the whole gateway.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/src/gateway/Gateway.ts, line 56:
<comment>Missing error isolation in merged incoming stream: `Stream.mergeAll` terminates the entire stream when any single adapter stream fails. A transient network error in the Telegram adapter would cut off incoming messages from all adapters. Wrap each adapter's stream with `Stream.catchAll` or `Stream.catchTag` to isolate failures per adapter (log and resume, or restart the adapter stream) so one adapter's error doesn't bring down the whole gateway.</comment>
<file context>
@@ -0,0 +1,93 @@
+ });
+ }
+
+ const incoming = Stream.mergeAll(
+ adapters.map((adapter) => adapter.incoming),
+ { concurrency: "unbounded", bufferSize: 16 },
</file context>
|
|
||
| const retrySchedule = Schedule.min([ | ||
| Schedule.exponential(settings.retryBaseDelayMillis), | ||
| Schedule.spaced(Math.max(settings.retryBaseDelayMillis, settings.retryMaxDelayMillis)), |
There was a problem hiding this comment.
P2: A configured retry maximum below the base delay is ignored, so polling can wait longer than retryMaxDelayMillis. Use the configured maximum as the cap (or reject this invalid config combination).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/src/gateway/adapters/telegram/TelegramAdapter.ts, line 286:
<comment>A configured retry maximum below the base delay is ignored, so polling can wait longer than `retryMaxDelayMillis`. Use the configured maximum as the cap (or reject this invalid config combination).</comment>
<file context>
@@ -0,0 +1,353 @@
+
+ const retrySchedule = Schedule.min([
+ Schedule.exponential(settings.retryBaseDelayMillis),
+ Schedule.spaced(Math.max(settings.retryBaseDelayMillis, settings.retryMaxDelayMillis)),
+ ]).pipe(
+ Schedule.tap(({ input }) =>
</file context>
|
|
||
| const TelegramFileFields = { | ||
| file_id: Schema.String, | ||
| file_unique_id: Schema.optionalKey(Schema.String), |
There was a problem hiding this comment.
P2: file_unique_id in TelegramFileFields is modeled with Schema.optionalKey, but the Telegram Bot API guarantees file_unique_id is always present on all file objects (File, PhotoSize, Document, Audio, Video, Voice). This means the TypeScript type will have file_unique_id?: string | undefined when Telegram never omits it, forcing unnecessary defensive ?? / ?. checks in consumer code. Use Schema.String instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/src/gateway/adapters/telegram/TelegramModels.ts, line 5:
<comment>`file_unique_id` in `TelegramFileFields` is modeled with `Schema.optionalKey`, but the Telegram Bot API guarantees `file_unique_id` is always present on all file objects (`File`, `PhotoSize`, `Document`, `Audio`, `Video`, `Voice`). This means the TypeScript type will have `file_unique_id?: string | undefined` when Telegram never omits it, forcing unnecessary defensive `??` / `?.` checks in consumer code. Use `Schema.String` instead.</comment>
<file context>
@@ -0,0 +1,103 @@
+
+const TelegramFileFields = {
+ file_id: Schema.String,
+ file_unique_id: Schema.optionalKey(Schema.String),
+ file_size: Schema.optionalKey(Schema.Finite),
+};
</file context>
| }; | ||
|
|
||
| export const TelegramUser = Schema.Struct({ | ||
| id: Schema.Finite, |
There was a problem hiding this comment.
P3: Numeric fields like id, message_id, date, width, height, duration, and file_size use Schema.Finite, which accepts any finite number including floats. The Telegram Bot API always returns integers for these fields. The codebase already has const NonNegativeInteger = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)) in packages/contracts/src/gateway.ts — using that pattern (or at minimum Schema.Finite.check(Schema.isInt())) here would catch non-integer values that Schema.Finite silently accepts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/src/gateway/adapters/telegram/TelegramModels.ts, line 10:
<comment>Numeric fields like `id`, `message_id`, `date`, `width`, `height`, `duration`, and `file_size` use `Schema.Finite`, which accepts any finite number including floats. The Telegram Bot API always returns integers for these fields. The codebase already has `const NonNegativeInteger = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))` in `packages/contracts/src/gateway.ts` — using that pattern (or at minimum `Schema.Finite.check(Schema.isInt())`) here would catch non-integer values that `Schema.Finite` silently accepts.</comment>
<file context>
@@ -0,0 +1,103 @@
+};
+
+export const TelegramUser = Schema.Struct({
+ id: Schema.Finite,
+ is_bot: Schema.Boolean,
+ first_name: Schema.String,
</file context>
What changed
~/.compass/gateway.jsonloading with non-secret Telegram polling configuration and deny-by-default allowed channels~/.compass/files, including streamed inbound downloads and configurable size limits@compass/contractswith dedicated contract tests.plans/02-chat-gateway.htmlWhy
Compass needs a small reusable chat transport boundary without the broader Vercel Chat SDK feature set. This keeps platform behavior behind adapters while preserving Effect-native resource management, typed failures, backpressure, and testability.
Impact
Applications can consume a unified incoming stream and send finished messages or files to allowlisted channels. Telegram is supported now through polling; future adapters can implement the same platform-neutral contract.
Validation
vp run readyvp run typecheck